import File from './file/File';
import Context from './Context';
import { Dict, ZPackOptions, AssemblyType } from './Types';
import BundleFile from './file/BundleFile';
import ModuleFile from './file/ModuleFile';
import { joinPath } from './Utils';
import TemplateFile from './file/TemplateFile';
import BootFile from './file/BootFile';

export default class Assembly {

    public cx: Context;
    public opt: ZPackOptions;

    constructor(cx: Context) {
        this.cx = cx;
        this.opt = cx.options;
    }

    public async push(file: File) {
        await file.load();
        let vfile = file.toVinylFile();
        if (vfile && this.cx.transform) {
            this.cx.transform.push(vfile);
            if (this.cx.options.printWrite) {
                console.log('>>', file.type, vfile.relative);
            }
        }
        for (let f of file.extraFiles) {
            await this.push(f);
        }
    }

    public async recursiveLoad(entry: File, action: (f: File) => any = _ => true) {
        let files: Dict<File> = {};
        let list: File[] = [];
        for (let file: File | undefined = entry; file; file = list.pop()) {
            files[file.id] = file;
            if (action(file) === false) {
                continue;
            }
            await file.load();
            for (let dependency of Object.values(file.dependencies)) {
                if (files[dependency.id] == null) {
                    list.push(dependency);
                }
            }
        }
        return files;
    }

    public async onFile(entry: File): Promise<void> {
        //
    }

    public async onFlush(): Promise<void> {
        if (!this.cx.files['ZPack.js']) {
            let zpackStubJs = new TemplateFile(this.cx, {
                src: joinPath(__dirname, 'rt', 'ZPack.js'),
                id: 'ZPack.js'
            });
            await this.push(zpackStubJs);
        }
        //this.cx.bootDependencies.push(zpackStubJs);
        // if (this.cx.options.needBootFile) {
        //     let bootFile = new BootFile(this.cx);
        //     await this.push(bootFile);
        // }
    }

    public static create(cx: Context, name: AssemblyType) {
        switch (name) {
            case 'single': return new SingleFiles(cx);
            case 'allInOne': return new AllInOne(cx);
            case 'modules': return new PrepareModules(cx);
            case 'modulesAllInOne': return new PrepareModulesAllInOne(cx);
            case 'recursive': return new RecursiveAll(cx);
            case 'recursiveSourceOnly': return new RecursiveSourceOnly(cx);
            default: return new SingleFiles(cx);
        }
    }
}

class SingleFiles extends Assembly {
    public async onFile(f: File) {
        await this.push(f);
    }
}

class RecursiveAll extends Assembly {
    public async onFile(entry: File) {
        let bundle = new BundleFile(this.cx, entry.id);
        await this.recursiveLoad(entry, f => bundle.files.push(f));
        await this.push(bundle);
    }
}

class RecursiveSourceOnly extends Assembly {
    public async onFile(entry: File) {
        let bundle = new BundleFile(this.cx, entry.id);
        await this.recursiveLoad(entry, f => f.type === FileType.source && bundle.files.push(f));
        await this.push(bundle);
    }
}

class PrepareModules extends Assembly {
    public async onFile(entry: File) {
        await this.recursiveLoad(entry);
        await super.onFile(entry);
    }

    public async onFlush() {
        let bundles: Dict<BundleFile> = {};
        for (let file of Object.values(this.cx.files)) {
            if (file.type !== FileType.module) {
                continue;
            }
            let id = joinPath(
                ModuleFile.NODE_MODULES_PATH,
                (file as ModuleFile).name,
            );
            let bundle = bundles[id];
            if (bundle == null) {
                bundle = bundles[id] = new BundleFile(this.cx, id);
            }
            bundle.files.push(file);
        }
        for (let bundle of Object.values(bundles)) {
            await this.push(bundle);
        }
        await super.onFlush();
    }
}

class PrepareModulesAllInOne extends Assembly {
    public async onFile(entry: File) {
        await this.recursiveLoad(entry);
    }

    public async onFlush() {
        let bundle = new BundleFile(this.cx, ModuleFile.NODE_MODULES_PATH + 'all');
        for (let file of Object.values(this.cx.files)) {
            if (file.type !== FileType.module) {
                continue;
            }
            bundle.files.push(file);
        }
        await this.push(bundle);
        await super.onFlush();
    }
}

class AllInOne extends Assembly {
    public async onFile(entry: File) {
        await this.recursiveLoad(entry);
    }

    public async onFlush() {
        let bundle = new BundleFile(this.cx, 'all');
        for (let file of Object.values(this.cx.files)) {
            bundle.files.push(file);
        }
        await this.push(bundle);
        await super.onFlush();
    }
}
